package tos

import (
	"bytes"
	"context"
	"sync"
	"time"

	bytedtracer "code.byted.org/bytedtrace/interface-go"
	"code.byted.org/bytertc/whiteboard_server/biz/pkg/biz_error"
	"code.byted.org/bytertc/whiteboard_server/biz/pkg/trace"
	"code.byted.org/gopkg/logs/v2/log"
	"code.byted.org/gopkg/tos"
	"github.com/cenkalti/backoff/v4"
	"github.com/pkg/errors"
)

//go:generate mockgen -destination ../../mock/tos/uploader.go . Uploader

type Uploader interface {
	Upload(ctx context.Context, fileName string, binaryData []byte) (err error)
}

var uploaderMap = sync.Map{}

const contentType = `application/x-protobuf`

func GetUploader(ctx context.Context, bucket, ak string) (Uploader, error) {
	u, exist := uploaderMap.Load(bucket)
	if exist {
		return u.(Uploader), nil
	}

	log.V1.CtxInfo(ctx, "create new TOS client for bucket %s", bucket)
	client, err := tos.NewTos(tos.WithBucket(bucket), tos.WithCredentials(&tos.BucketAccessKeyCredentials{
		BucketName: bucket,
		AccessKey:  ak,
	}))
	if err != nil {
		return nil, errors.WithStack(err)
	}

	up := &uploader{
		bucket: bucket,
		client: client,
	}
	uploaderMap.Store(bucket, up)
	return up, nil
}

type uploader struct {
	bucket string
	client *tos.Tos
}

func (u *uploader) Upload(ctx context.Context, fileName string, binaryData []byte) (err error) {
	span, ctx := bytedtracer.StartCustomSpan(ctx, trace.INTERNAL, trace.CompactUpload,
		bytedtracer.EnableEmitSpanMetrics)
	length := len(binaryData)
	defer func() {
		span.SetTags(bytedtracer.NewTagKV(trace.IsError, err != nil),
			bytedtracer.NewTagKV(trace.BucketName, u.bucket),
			bytedtracer.NewTagKV(trace.DataLength, length))
		span.Finish()
	}()

	strategy := backoff.WithMaxRetries(backoff.WithContext(backoff.NewConstantBackOff(time.Millisecond*50), ctx), 3)
	err = backoff.RetryNotify(func() error {
		return u.client.PutObject(ctx, fileName, int64(length), bytes.NewReader(binaryData),
			tos.ContentType(contentType))
	}, strategy, func(err error, duration time.Duration) {
		log.V1.CtxWarn(ctx, "fail to upload %s to %s, len %d, err %+v", fileName, u.bucket, length, err)
	})
	return biz_error.UploadError.Wrap(errors.WithStack(err))
}
